| import axios from 'axios'; |
| import { log } from '../utils/logger.js'; |
| import { generateSessionId, generateProjectId, generateTokenId, generateInstanceId } from '../utils/idGenerator.js'; |
| import config, { getConfigJson } from '../config/config.js'; |
| import { OAUTH_CONFIG } from '../constants/oauth.js'; |
| import { buildAxiosRequestConfig } from '../utils/httpClient.js'; |
| import { |
| DEFAULT_REQUEST_COUNT_PER_TOKEN, |
| TOKEN_REFRESH_BUFFER |
| } from '../constants/index.js'; |
| import TokenStore from './token_store.js'; |
| import { TokenError } from '../utils/errors.js'; |
| import quotaManager from './quota_manager.js'; |
| import tokenCooldownManager from './token_cooldown_manager.js'; |
| import { randomUUID } from 'crypto'; |
|
|
| |
| const RotationStrategy = { |
| ROUND_ROBIN: 'round_robin', |
| QUOTA_EXHAUSTED: 'quota_exhausted', |
| REQUEST_COUNT: 'request_count' |
| }; |
|
|
| |
| |
| |
| |
| class TokenManager { |
| |
| |
| |
| constructor(filePath) { |
| this.store = new TokenStore(filePath); |
| |
| this.tokens = []; |
| |
| this.currentIndex = 0; |
|
|
| |
| |
| this.rotationStrategy = RotationStrategy.ROUND_ROBIN; |
| |
| this.requestCountPerToken = DEFAULT_REQUEST_COUNT_PER_TOKEN; |
| |
| this.tokenRequestCounts = new Map(); |
|
|
| |
| |
| this.availableQuotaTokenIndices = []; |
| |
| this.currentQuotaIndex = 0; |
|
|
| |
| this._initPromise = null; |
| } |
|
|
| async _initialize() { |
| try { |
| log.info('正在初始化token管理器...'); |
| const tokenArray = await this.store.readAll(); |
|
|
| this.tokens = tokenArray.filter(token => token.enable !== false).map(token => ({ |
| ...token, |
| sessionId: generateSessionId(), |
| instanceId: generateInstanceId(), |
| deviceId: randomUUID(), |
| sub: token?.sub ? token?.sub : "g1-pro-tier" |
| })); |
|
|
| this.currentIndex = 0; |
| this.tokenRequestCounts.clear(); |
| this._rebuildAvailableQuotaTokens(); |
|
|
| |
| this.loadRotationConfig(); |
|
|
| if (this.tokens.length === 0) { |
| log.warn('⚠ 暂无可用账号,请使用以下方式添加:'); |
| log.warn(' 方式1: 运行 npm run login 命令登录'); |
| log.warn(' 方式2: 访问前端管理页面添加账号'); |
| } else { |
| log.info(`成功加载 ${this.tokens.length} 个可用token`); |
| if (this.rotationStrategy === RotationStrategy.REQUEST_COUNT) { |
| log.info(`轮询策略: ${this.rotationStrategy}, 每token请求 ${this.requestCountPerToken} 次后切换`); |
| } else { |
| log.info(`轮询策略: ${this.rotationStrategy}`); |
| } |
|
|
| |
| await this._refreshExpiredTokensConcurrently(); |
| } |
| } catch (error) { |
| log.error('初始化token失败:', error.message); |
| this.tokens = []; |
| } |
| } |
|
|
| |
| |
| |
| |
| async _refreshExpiredTokensConcurrently() { |
| const expiredTokens = this.tokens.filter(token => this.isExpired(token)); |
| if (expiredTokens.length === 0) { |
| return; |
| } |
|
|
| |
| const salt = await this.store.getSalt(); |
| const tokenIds = expiredTokens.map(token => generateTokenId(token.refresh_token, salt)); |
|
|
| log.info(`正在批量刷新 ${tokenIds.length} 个token: ${tokenIds.join(', ')}`); |
| const startTime = Date.now(); |
|
|
| const results = await Promise.allSettled( |
| expiredTokens.map(token => this._refreshTokenSafe(token)) |
| ); |
|
|
| let successCount = 0; |
| let failCount = 0; |
| const tokensToDisable = []; |
| const failedTokenIds = []; |
|
|
| results.forEach((result, index) => { |
| const token = expiredTokens[index]; |
| const tokenId = tokenIds[index]; |
| if (result.status === 'fulfilled') { |
| if (result.value === 'success') { |
| successCount++; |
| } else if (result.value === 'disable') { |
| tokensToDisable.push(token); |
| failCount++; |
| failedTokenIds.push(tokenId); |
| } |
| } else { |
| failCount++; |
| failedTokenIds.push(tokenId); |
| } |
| }); |
|
|
| |
| for (const token of tokensToDisable) { |
| this.disableToken(token); |
| } |
|
|
| const elapsed = Date.now() - startTime; |
| if (failCount > 0) { |
| log.warn(`刷新完成: 成功 ${successCount}, 失败 ${failCount} (${failedTokenIds.join(', ')}), 耗时 ${elapsed}ms`); |
| } else { |
| log.info(`刷新完成: 成功 ${successCount}, 耗时 ${elapsed}ms`); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async _refreshTokenSafe(token) { |
| try { |
| |
| await this.refreshToken(token, true); |
| return 'success'; |
| } catch (error) { |
| if (error.statusCode === 403 || error.statusCode === 400) { |
| return 'disable'; |
| } |
| throw error; |
| } |
| } |
|
|
| async _ensureInitialized() { |
| if (!this._initPromise) { |
| this._initPromise = this._initialize(); |
| } |
| return this._initPromise; |
| } |
|
|
| |
| loadRotationConfig() { |
| try { |
| const jsonConfig = getConfigJson(); |
| if (jsonConfig.rotation) { |
| this.rotationStrategy = jsonConfig.rotation.strategy || RotationStrategy.ROUND_ROBIN; |
| this.requestCountPerToken = jsonConfig.rotation.requestCount || 10; |
| } |
| } catch (error) { |
| log.warn('加载轮询配置失败,使用默认值:', error.message); |
| } |
| } |
|
|
| |
| updateRotationConfig(strategy, requestCount) { |
| if (strategy && Object.values(RotationStrategy).includes(strategy)) { |
| this.rotationStrategy = strategy; |
| } |
| if (requestCount && requestCount > 0) { |
| this.requestCountPerToken = requestCount; |
| } |
| |
| this.tokenRequestCounts.clear(); |
| if (this.rotationStrategy === RotationStrategy.REQUEST_COUNT) { |
| log.info(`轮询策略已更新: ${this.rotationStrategy}, 每token请求 ${this.requestCountPerToken} 次后切换`); |
| } else { |
| log.info(`轮询策略已更新: ${this.rotationStrategy}`); |
| } |
| } |
|
|
| |
| _rebuildAvailableQuotaTokens() { |
| this.availableQuotaTokenIndices = []; |
| this.tokens.forEach((token, index) => { |
| if (token.enable !== false && token.hasQuota !== false) { |
| this.availableQuotaTokenIndices.push(index); |
| } |
| }); |
|
|
| if (this.availableQuotaTokenIndices.length === 0) { |
| this.currentQuotaIndex = 0; |
| } else { |
| this.currentQuotaIndex = this.currentQuotaIndex % this.availableQuotaTokenIndices.length; |
| } |
| } |
|
|
| |
| _removeQuotaIndex(tokenIndex) { |
| const pos = this.availableQuotaTokenIndices.indexOf(tokenIndex); |
| if (pos !== -1) { |
| this.availableQuotaTokenIndices.splice(pos, 1); |
| if (this.currentQuotaIndex >= this.availableQuotaTokenIndices.length) { |
| this.currentQuotaIndex = 0; |
| } |
| } |
| } |
|
|
| async fetchProjectId(token) { |
| |
| try { |
|
|
| const {projectId,sub} = await this._tryLoadCodeAssist(token) || {}; |
| if (projectId) return {projectId,sub}; |
| log.warn('[fetchProjectId] loadCodeAssist 未返回 projectId,回退到 onboardUser'); |
| } catch (err) { |
| log.warn(`[fetchProjectId] loadCodeAssist 失败: ${err.message},回退到 onboardUser`); |
| } |
|
|
| |
| try { |
| const {projectId,sub} = await this._tryOnboardUser(token) || {}; |
| if (projectId) return {projectId, sub}; |
| log.error('[fetchProjectId] loadCodeAssist 和 onboardUser 均未能获取 projectId'); |
| return {projectId: undefined, sub: "free-tier"}; |
| } catch (err) { |
| log.error(`[fetchProjectId] onboardUser 失败: ${err.message}`); |
| return {projectId: undefined, sub: "free-tier"}; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async _tryLoadCodeAssist(token) { |
| const apiHost = config.api.host; |
| const requestUrl = `https://${apiHost}/v1internal:loadCodeAssist`; |
| const requestBody = { |
| metadata: { |
| ideType: 'ANTIGRAVITY', |
| platform: 'PLATFORM_UNSPECIFIED', |
| pluginType: 'GEMINI' |
| } |
| }; |
|
|
| log.info(`[loadCodeAssist] 请求: ${requestUrl}`); |
| const response = await axios(buildAxiosRequestConfig({ |
| method: 'POST', |
| url: requestUrl, |
| headers: { |
| 'Host': apiHost, |
| 'User-Agent': config.api.userAgent, |
| 'Authorization': `Bearer ${token.access_token}`, |
| 'Content-Type': 'application/json', |
| 'Accept-Encoding': 'gzip' |
| }, |
| data: JSON.stringify(requestBody) |
| })); |
|
|
| const data = response.data; |
| |
|
|
| |
| let sub = "free-tier"; |
| if (data?.currentTier) { |
| log.info('[loadCodeAssist] 用户已激活'); |
| const projectId = data.cloudaicompanionProject; |
| if (projectId) { |
| log.info(`[loadCodeAssist] 成功获取 projectId: ${projectId}`); |
| sub = data.currentTier.id; |
| return {projectId, sub}; |
| } |
| log.warn('[loadCodeAssist] 响应中无 projectId'); |
| return null; |
| } |
|
|
| log.info('[loadCodeAssist] 用户未激活 (无 currentTier)'); |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async _tryOnboardUser(token) { |
| const apiHost = config.api.host; |
| const requestUrl = `https://${apiHost}/v1internal:onboardUser`; |
|
|
| |
| const tierId = await this._getOnboardTier(token); |
| if (!tierId) { |
| log.error('[onboardUser] 无法确定用户 tier'); |
| return null; |
| } |
|
|
| log.info(`[onboardUser] 用户 tier: ${tierId}`); |
|
|
| const requestBody = { |
| tierId: tierId, |
| metadata: { |
| ideType: 'ANTIGRAVITY', |
| platform: 'PLATFORM_UNSPECIFIED', |
| pluginType: 'GEMINI' |
| } |
| }; |
|
|
| log.info(`[onboardUser] 请求: ${requestUrl}`); |
|
|
| |
| const maxAttempts = 5; |
| for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
| log.info(`[onboardUser] 轮询尝试 ${attempt}/${maxAttempts}`); |
|
|
| const response = await axios(buildAxiosRequestConfig({ |
| method: 'POST', |
| url: requestUrl, |
| headers: { |
| 'Host': apiHost, |
| 'User-Agent': config.api.userAgent, |
| 'Authorization': `Bearer ${token.access_token}`, |
| 'Content-Type': 'application/json', |
| 'Accept-Encoding': 'gzip' |
| }, |
| data: JSON.stringify(requestBody), |
| timeout: 30000 |
| })); |
|
|
| const data = response.data; |
| |
|
|
| |
| let sub = "g1-pro-tier"; |
| if (data?.done) { |
| log.info('[onboardUser] 操作完成'); |
| const responseData = data.response || {}; |
| const projectObj = responseData.cloudaicompanionProject; |
|
|
| let projectId = null; |
| if (typeof projectObj === 'object' && projectObj !== null) { |
| projectId = projectObj.id; |
| } else if (typeof projectObj === 'string') { |
| projectId = projectObj; |
| } |
|
|
| if (projectId) { |
| log.info(`[onboardUser] 成功获取 projectId: ${projectId}`); |
| return {projectId,sub}; |
| } |
| log.warn('[onboardUser] 操作完成但响应中无 projectId'); |
| return null; |
| } |
|
|
| log.info('[onboardUser] 操作进行中,等待 2 秒...'); |
| await new Promise(resolve => setTimeout(resolve, 2000)); |
| } |
|
|
| log.error('[onboardUser] 超时:操作未在 10 秒内完成'); |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async _getOnboardTier(token) { |
| const apiHost = config.api.host; |
| const requestUrl = `https://${apiHost}/v1internal:loadCodeAssist`; |
| const requestBody = { |
| metadata: { |
| ideType: 'ANTIGRAVITY', |
| platform: 'PLATFORM_UNSPECIFIED', |
| pluginType: 'GEMINI' |
| } |
| }; |
|
|
| log.info(`[_getOnboardTier] 请求: ${requestUrl}`); |
|
|
| try { |
| const response = await axios(buildAxiosRequestConfig({ |
| method: 'POST', |
| url: requestUrl, |
| headers: { |
| 'Host': apiHost, |
| 'User-Agent': config.api.userAgent, |
| 'Authorization': `Bearer ${token.access_token}`, |
| 'Content-Type': 'application/json', |
| 'Accept-Encoding': 'gzip' |
| }, |
| data: JSON.stringify(requestBody), |
| timeout: 30000 |
| })); |
|
|
| const data = response.data; |
| |
|
|
| |
| const allowedTiers = data?.allowedTiers || []; |
| for (const tier of allowedTiers) { |
| if (tier.isDefault) { |
| log.info(`[_getOnboardTier] 找到默认 tier: ${tier.id}`); |
| return tier.id; |
| } |
| } |
|
|
| |
| log.warn('[_getOnboardTier] 未找到默认 tier,使用 LEGACY'); |
| return 'LEGACY'; |
| } catch (err) { |
| log.error(`[_getOnboardTier] 获取 tier 失败: ${err.message}`); |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async fetchProjectIdForToken(tokenId) { |
| const tokenData = await this.findTokenById(tokenId); |
| if (!tokenData) { |
| throw new TokenError('Token不存在', null, 404); |
| } |
|
|
| |
| if (this.isExpired(tokenData)) { |
| await this.refreshToken(tokenData); |
| } |
|
|
| const {projectId,sub} = await this.fetchProjectId(tokenData) || {}; |
| if (!projectId) { |
| throw new TokenError('无法获取 projectId,该账号可能无资格', null, 400); |
| } |
|
|
| |
| tokenData.projectId = projectId; |
| tokenData.sub = sub; |
| tokenData.hasQuota = true; |
| this.saveToFile(tokenData); |
|
|
| |
| const memoryToken = this.tokens.find(t => t.refresh_token === tokenData.refresh_token); |
| if (memoryToken) { |
| memoryToken.projectId = projectId; |
| memoryToken.sub = sub; |
| memoryToken.hasQuota = true; |
| } |
|
|
| return { projectId }; |
| } |
|
|
| |
| |
| |
| |
| |
| isExpired(token) { |
| if (!token.timestamp || !token.expires_in) return true; |
| const expiresAt = token.timestamp + (token.expires_in * 1000); |
| return Date.now() >= expiresAt - TOKEN_REFRESH_BUFFER; |
| } |
|
|
| async refreshToken(token, silent = false) { |
| |
| const salt = await this.store.getSalt(); |
| const tokenId = generateTokenId(token.refresh_token, salt); |
| if (!silent) { |
| log.info(`正在刷新token: ${tokenId}`); |
| } |
|
|
| const body = new URLSearchParams({ |
| client_id: OAUTH_CONFIG.CLIENT_ID, |
| client_secret: OAUTH_CONFIG.CLIENT_SECRET, |
| grant_type: 'refresh_token', |
| refresh_token: token.refresh_token |
| }); |
|
|
| try { |
| const response = await axios(buildAxiosRequestConfig({ |
| method: 'POST', |
| url: OAUTH_CONFIG.TOKEN_URL, |
| headers: { |
| 'Host': 'oauth2.googleapis.com', |
| 'User-Agent': 'Go-http-client/1.1', |
| 'Content-Type': 'application/x-www-form-urlencoded', |
| 'Accept-Encoding': 'gzip' |
| }, |
| data: body.toString() |
| })); |
|
|
| token.access_token = response.data.access_token; |
| token.expires_in = response.data.expires_in; |
| token.timestamp = Date.now(); |
| this.saveToFile(token); |
| return token; |
| } catch (error) { |
| const statusCode = error.response?.status; |
| const rawBody = error.response?.data; |
| const message = typeof rawBody === 'string' ? rawBody : (rawBody?.error?.message || error.message || '刷新 token 失败'); |
| throw new TokenError(message, tokenId, statusCode || 500); |
| } |
| } |
|
|
| saveToFile(tokenToUpdate = null) { |
| |
| this.store.mergeActiveTokens(this.tokens, tokenToUpdate).catch((error) => { |
| log.error('保存账号配置文件失败:', error.message); |
| }); |
| } |
|
|
| disableToken(token) { |
| log.warn(`禁用token ...${token.access_token.slice(-8)}`) |
| token.enable = false; |
| this.saveToFile(); |
| |
| this.tokenRequestCounts.delete(token.refresh_token); |
| this.tokens = this.tokens.filter(t => t.refresh_token !== token.refresh_token); |
| this.currentIndex = this.currentIndex % Math.max(this.tokens.length, 1); |
| |
| this._rebuildAvailableQuotaTokens(); |
| } |
|
|
| |
| incrementRequestCount(tokenKey) { |
| const current = this.tokenRequestCounts.get(tokenKey) || 0; |
| const newCount = current + 1; |
| this.tokenRequestCounts.set(tokenKey, newCount); |
| return newCount; |
| } |
|
|
| |
| resetRequestCount(tokenKey) { |
| this.tokenRequestCounts.set(tokenKey, 0); |
| } |
|
|
|
|
| |
| markQuotaExhausted(token) { |
| token.hasQuota = false; |
| this.saveToFile(token); |
| log.warn(`...${token.access_token.slice(-8)}: 额度已耗尽,标记为无额度`); |
|
|
| if (this.rotationStrategy === RotationStrategy.QUOTA_EXHAUSTED) { |
| const tokenIndex = this.tokens.findIndex(t => t.refresh_token === token.refresh_token); |
| if (tokenIndex !== -1) { |
| this._removeQuotaIndex(tokenIndex); |
| } |
| this.currentIndex = (this.currentIndex + 1) % Math.max(this.tokens.length, 1); |
| } |
| } |
|
|
| |
| restoreQuota(token) { |
| token.hasQuota = true; |
| this.saveToFile(token); |
| log.info(`...${token.access_token.slice(-8)}: 额度已恢复`); |
| } |
|
|
| |
| |
| |
| |
| |
| async recordRequest(token, modelId) { |
| if (!token || !modelId) return; |
|
|
| try { |
| const salt = await this.store.getSalt(); |
| const tokenId = generateTokenId(token.refresh_token, salt); |
| quotaManager.recordRequest(tokenId, modelId); |
| } catch (error) { |
| |
| log.warn('记录请求次数失败:', error.message); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async _prepareToken(token) { |
| |
| if (this.isExpired(token)) { |
| await this.refreshToken(token); |
| } |
|
|
| |
| if (!token.projectId) { |
| if (config.skipProjectIdFetch) { |
| token.projectId = generateProjectId(); |
| this.saveToFile(token); |
| log.info(`...${token.access_token.slice(-8)}: 使用随机生成的projectId: ${token.projectId}`); |
| } else { |
| const {projectId,sub} = await this.fetchProjectId(token) || {}; |
| if (projectId === undefined) { |
| log.warn(`...${token.access_token.slice(-8)}: 无资格获取projectId,禁用账号`); |
| return 'disable'; |
| } |
| token.projectId = projectId; |
| token.sub = sub; |
| this.saveToFile(token); |
| } |
| } |
|
|
| return 'ready'; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| _handleTokenError(error, token) { |
| const suffix = token.access_token?.slice(-8) || 'unknown'; |
| if (error.statusCode === 403 || error.statusCode === 400) { |
| log.warn(`...${suffix}: Token 已失效或错误,已自动禁用该账号`); |
| return 'disable'; |
| } |
| log.error(`...${suffix} 操作失败:`, error.message); |
| return 'skip'; |
| } |
|
|
| |
| |
| |
| |
| _resetAllQuotas() { |
| log.warn('所有token额度已耗尽,重置额度状态'); |
| this.tokens.forEach(t => { |
| t.hasQuota = true; |
| }); |
| this.saveToFile(); |
| this._rebuildAvailableQuotaTokens(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| _checkAllTokensExhaustedForModel(modelId) { |
| if (!modelId || this.tokens.length === 0) return false; |
|
|
| for (const token of this.tokens) { |
| if (this._canUseTokenForModel(token, modelId)) { |
| return false; |
| } |
| } |
| return true; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| _hasQuotaForModel(token, modelId) { |
| if (!token || !modelId) return true; |
|
|
| try { |
| const salt = this.store._salt; |
| if (!salt) return true; |
|
|
| const tokenId = generateTokenId(token.refresh_token, salt); |
| return quotaManager.hasQuotaForModel(tokenId, modelId); |
| } catch (error) { |
| |
| return true; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| _isTokenAvailableForModel(token, modelId) { |
| if (!token || !modelId) return true; |
|
|
| try { |
| const salt = this.store._salt; |
| if (!salt) return true; |
|
|
| const tokenId = generateTokenId(token.refresh_token, salt); |
| return tokenCooldownManager.isAvailable(tokenId, modelId); |
| } catch (error) { |
| return true; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| _canUseTokenForModel(token, modelId) { |
| if (!token || !modelId) return true; |
|
|
| |
| if (!this._isTokenAvailableForModel(token, modelId)) { |
| return false; |
| } |
|
|
| |
| return this._hasQuotaForModel(token, modelId); |
| } |
|
|
| |
| |
| |
| |
| |
| async getToken(modelId = null) { |
| await this._ensureInitialized(); |
| if (this.tokens.length === 0) return null; |
|
|
| |
| if (this.rotationStrategy === RotationStrategy.QUOTA_EXHAUSTED) { |
| return this._getTokenForQuotaExhaustedStrategy(modelId); |
| } |
|
|
| return this._getTokenForDefaultStrategy(modelId); |
| } |
|
|
| |
| |
| |
| |
| |
| async _getTokenForQuotaExhaustedStrategy(modelId = null) { |
| |
| if (this.availableQuotaTokenIndices.length === 0) { |
| this._resetAllQuotas(); |
| } |
|
|
| const totalAvailable = this.availableQuotaTokenIndices.length; |
| if (totalAvailable === 0) { |
| return null; |
| } |
|
|
| |
| let allTokensExhausted = false; |
| if (modelId) { |
| allTokensExhausted = this._checkAllTokensExhaustedForModel(modelId); |
| } |
|
|
| const startIndex = this.currentQuotaIndex % totalAvailable; |
|
|
| for (let i = 0; i < totalAvailable; i++) { |
| const listIndex = (startIndex + i) % totalAvailable; |
| const tokenIndex = this.availableQuotaTokenIndices[listIndex]; |
| const token = this.tokens[tokenIndex]; |
|
|
| |
| if (modelId && !allTokensExhausted) { |
| if (!this._canUseTokenForModel(token, modelId)) { |
| |
| continue; |
| } |
| } |
|
|
| try { |
| const result = await this._prepareToken(token); |
| if (result === 'disable') { |
| this.disableToken(token); |
| this._rebuildAvailableQuotaTokens(); |
| if (this.tokens.length === 0 || this.availableQuotaTokenIndices.length === 0) { |
| return null; |
| } |
| continue; |
| } |
|
|
| this.currentIndex = tokenIndex; |
| this.currentQuotaIndex = listIndex; |
| return token; |
| } catch (error) { |
| const action = this._handleTokenError(error, token); |
| if (action === 'disable') { |
| this.disableToken(token); |
| this._rebuildAvailableQuotaTokens(); |
| if (this.tokens.length === 0 || this.availableQuotaTokenIndices.length === 0) { |
| return null; |
| } |
| } |
| |
| } |
| } |
|
|
| |
| this._resetAllQuotas(); |
| return this.tokens[0] || null; |
| } |
|
|
| |
| |
| |
| |
| |
| async _getTokenForDefaultStrategy(modelId = null) { |
| const totalTokens = this.tokens.length; |
| const startIndex = this.currentIndex; |
|
|
| |
| let allTokensExhausted = false; |
| if (modelId) { |
| allTokensExhausted = this._checkAllTokensExhaustedForModel(modelId); |
| } |
|
|
| for (let i = 0; i < totalTokens; i++) { |
| const index = (startIndex + i) % totalTokens; |
| const token = this.tokens[index]; |
|
|
| |
| if (modelId && !allTokensExhausted) { |
| if (!this._canUseTokenForModel(token, modelId)) { |
| |
| continue; |
| } |
| } |
|
|
| try { |
| const result = await this._prepareToken(token); |
| if (result === 'disable') { |
| this.disableToken(token); |
| if (this.tokens.length === 0) return null; |
| continue; |
| } |
|
|
| |
| this.currentIndex = index; |
|
|
| |
| if (this.rotationStrategy === RotationStrategy.ROUND_ROBIN) { |
| this.currentIndex = (this.currentIndex + 1) % this.tokens.length; |
| } else if (this.rotationStrategy === RotationStrategy.REQUEST_COUNT) { |
| |
| const tokenKey = token.refresh_token; |
| const count = this.tokenRequestCounts.get(tokenKey) || 0; |
| if (count >= this.requestCountPerToken) { |
| this.resetRequestCount(tokenKey); |
| this.currentIndex = (this.currentIndex + 1) % this.tokens.length; |
| } |
| } |
|
|
| return token; |
| } catch (error) { |
| const action = this._handleTokenError(error, token); |
| if (action === 'disable') { |
| this.disableToken(token); |
| if (this.tokens.length === 0) return null; |
| } |
| |
| } |
| } |
|
|
| return null; |
| } |
|
|
| disableCurrentToken(token) { |
| const found = this.tokens.find(t => t.access_token === token.access_token); |
| if (found) { |
| this.disableToken(found); |
| } |
| } |
|
|
| |
| async reload() { |
| this._initPromise = this._initialize(); |
| await this._initPromise; |
| log.info('Token已热重载'); |
| } |
|
|
| async addToken(tokenData) { |
| try { |
| const allTokens = await this.store.readAll(); |
|
|
| const newToken = { |
| access_token: tokenData.access_token, |
| refresh_token: tokenData.refresh_token, |
| expires_in: tokenData.expires_in || 3599, |
| timestamp: tokenData.timestamp || Date.now(), |
| enable: tokenData.enable !== undefined ? tokenData.enable : true |
| }; |
|
|
| if (tokenData.projectId) { |
| newToken.projectId = tokenData.projectId; |
| } |
| if (tokenData.email) { |
| newToken.email = tokenData.email; |
| } |
| if (tokenData.hasQuota !== undefined) { |
| newToken.hasQuota = tokenData.hasQuota; |
| } |
| if (tokenData.sub) { |
| newToken.sub = tokenData.sub; |
| } |
|
|
| allTokens.push(newToken); |
| await this.store.writeAll(allTokens); |
|
|
| await this.reload(); |
| return { success: true, message: 'Token添加成功' }; |
| } catch (error) { |
| log.error('添加Token失败:', error.message); |
| return { success: false, message: error.message }; |
| } |
| } |
|
|
| async updateToken(refreshToken, updates) { |
| try { |
| const allTokens = await this.store.readAll(); |
|
|
| const index = allTokens.findIndex(t => t.refresh_token === refreshToken); |
| if (index === -1) { |
| return { success: false, message: 'Token不存在' }; |
| } |
|
|
| allTokens[index] = { ...allTokens[index], ...updates }; |
| await this.store.writeAll(allTokens); |
|
|
| await this.reload(); |
| return { success: true, message: 'Token更新成功' }; |
| } catch (error) { |
| log.error('更新Token失败:', error.message); |
| return { success: false, message: error.message }; |
| } |
| } |
|
|
| async deleteToken(refreshToken) { |
| try { |
| const allTokens = await this.store.readAll(); |
|
|
| const filteredTokens = allTokens.filter(t => t.refresh_token !== refreshToken); |
| if (filteredTokens.length === allTokens.length) { |
| return { success: false, message: 'Token不存在' }; |
| } |
|
|
| await this.store.writeAll(filteredTokens); |
|
|
| await this.reload(); |
| return { success: true, message: 'Token删除成功' }; |
| } catch (error) { |
| log.error('删除Token失败:', error.message); |
| return { success: false, message: error.message }; |
| } |
| } |
|
|
| async getTokenList() { |
| try { |
| const allTokens = await this.store.readAll(); |
| const salt = await this.store.getSalt(); |
|
|
| return allTokens.map(token => ({ |
| |
| id: generateTokenId(token.refresh_token, salt), |
| expires_in: token.expires_in, |
| timestamp: token.timestamp, |
| enable: token.enable !== false, |
| projectId: token.projectId || null, |
| email: token.email || null, |
| hasQuota: token.hasQuota !== false |
| })); |
| } catch (error) { |
| log.error('获取Token列表失败:', error.message); |
| return []; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async findTokenById(tokenId) { |
| try { |
| const allTokens = await this.store.readAll(); |
| const salt = await this.store.getSalt(); |
|
|
| return allTokens.find(token => |
| generateTokenId(token.refresh_token, salt) === tokenId |
| ) || null; |
| } catch (error) { |
| log.error('查找Token失败:', error.message); |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async updateTokenById(tokenId, updates) { |
| try { |
| const allTokens = await this.store.readAll(); |
| const salt = await this.store.getSalt(); |
|
|
| const index = allTokens.findIndex(token => |
| generateTokenId(token.refresh_token, salt) === tokenId |
| ); |
|
|
| if (index === -1) { |
| return { success: false, message: 'Token不存在' }; |
| } |
|
|
| allTokens[index] = { ...allTokens[index], ...updates }; |
| await this.store.writeAll(allTokens); |
|
|
| await this.reload(); |
| return { success: true, message: 'Token更新成功' }; |
| } catch (error) { |
| log.error('更新Token失败:', error.message); |
| return { success: false, message: error.message }; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async deleteTokenById(tokenId) { |
| try { |
| const allTokens = await this.store.readAll(); |
| const salt = await this.store.getSalt(); |
|
|
| const filteredTokens = allTokens.filter(token => |
| generateTokenId(token.refresh_token, salt) !== tokenId |
| ); |
|
|
| if (filteredTokens.length === allTokens.length) { |
| return { success: false, message: 'Token不存在' }; |
| } |
|
|
| await this.store.writeAll(filteredTokens); |
|
|
| await this.reload(); |
| return { success: true, message: 'Token删除成功' }; |
| } catch (error) { |
| log.error('删除Token失败:', error.message); |
| return { success: false, message: error.message }; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async refreshTokenById(tokenId) { |
| const tokenData = await this.findTokenById(tokenId); |
| if (!tokenData) { |
| throw new TokenError('Token不存在', null, 404); |
| } |
|
|
| const refreshedToken = await this.refreshToken(tokenData); |
| return { |
| expires_in: refreshedToken.expires_in, |
| timestamp: refreshedToken.timestamp |
| }; |
| } |
|
|
| |
| |
| |
| |
| async getSalt() { |
| return this.store.getSalt(); |
| } |
|
|
| |
| |
| |
| |
| |
| async getTokenId(token) { |
| if (!token?.refresh_token) return null; |
| try { |
| const salt = await this.store.getSalt(); |
| if (!salt) return null; |
| return generateTokenId(token.refresh_token, salt); |
| } catch (error) { |
| log.error(`生成tokenId失败: ${error.message}`); |
| return null; |
| } |
| } |
|
|
| |
| getRotationConfig() { |
| return { |
| strategy: this.rotationStrategy, |
| requestCount: this.requestCountPerToken, |
| currentIndex: this.currentIndex, |
| tokenCounts: Object.fromEntries(this.tokenRequestCounts) |
| }; |
| } |
| } |
|
|
| |
| export { RotationStrategy }; |
|
|
| const tokenManager = new TokenManager(); |
| export default tokenManager; |
|
|