mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
- 新增 accountBalanceService 与多 Provider 适配(Claude/Claude Console/OpenAI Responses/通用) - Redis 增加余额查询结果与本地统计缓存读写 - 管理端新增 /admin/accounts/balance 相关接口与汇总接口,并在应用启动时注册 Provider - 后台前端新增余额组件与 Dashboard 余额/配额汇总、低余额/高使用提示 - 补充 accountBalanceService 单元测试
134 lines
3.5 KiB
JavaScript
134 lines
3.5 KiB
JavaScript
const axios = require('axios')
|
||
const logger = require('../../utils/logger')
|
||
const ProxyHelper = require('../../utils/proxyHelper')
|
||
|
||
/**
|
||
* Provider 抽象基类
|
||
* 各平台 Provider 需继承并实现 queryBalance(account)
|
||
*/
|
||
class BaseBalanceProvider {
|
||
constructor(platform) {
|
||
this.platform = platform
|
||
this.logger = logger
|
||
}
|
||
|
||
/**
|
||
* 查询余额(抽象方法)
|
||
* @param {object} account - 账户对象
|
||
* @returns {Promise<object>}
|
||
* 形如:
|
||
* {
|
||
* balance: number|null,
|
||
* currency?: string,
|
||
* quota?: { daily, used, remaining, resetAt, percentage, unlimited? },
|
||
* queryMethod?: 'api'|'field'|'local',
|
||
* rawData?: any
|
||
* }
|
||
*/
|
||
async queryBalance(_account) {
|
||
throw new Error('queryBalance 方法必须由子类实现')
|
||
}
|
||
|
||
/**
|
||
* 通用 HTTP 请求方法(支持代理)
|
||
* @param {string} url
|
||
* @param {object} options
|
||
* @param {object} account
|
||
*/
|
||
async makeRequest(url, options = {}, account = {}) {
|
||
const config = {
|
||
url,
|
||
method: options.method || 'GET',
|
||
headers: options.headers || {},
|
||
timeout: options.timeout || 15000,
|
||
data: options.data,
|
||
params: options.params,
|
||
responseType: options.responseType
|
||
}
|
||
|
||
const proxyConfig = account.proxyConfig || account.proxy
|
||
if (proxyConfig) {
|
||
const agent = ProxyHelper.createProxyAgent(proxyConfig)
|
||
if (agent) {
|
||
config.httpAgent = agent
|
||
config.httpsAgent = agent
|
||
config.proxy = false
|
||
}
|
||
}
|
||
|
||
try {
|
||
const response = await axios(config)
|
||
return {
|
||
success: true,
|
||
data: response.data,
|
||
status: response.status,
|
||
headers: response.headers
|
||
}
|
||
} catch (error) {
|
||
const status = error.response?.status
|
||
const message = error.response?.data?.message || error.message || '请求失败'
|
||
this.logger.debug(`余额 Provider HTTP 请求失败: ${url} (${this.platform})`, {
|
||
status,
|
||
message
|
||
})
|
||
return { success: false, status, error: message }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从账户字段读取 dailyQuota / dailyUsage(通用降级方案)
|
||
* 注意:部分平台 dailyUsage 字段可能不是实时值,最终以 AccountBalanceService 的本地统计为准
|
||
*/
|
||
readQuotaFromFields(account) {
|
||
const dailyQuota = Number(account?.dailyQuota || 0)
|
||
const dailyUsage = Number(account?.dailyUsage || 0)
|
||
|
||
// 无限制
|
||
if (!Number.isFinite(dailyQuota) || dailyQuota <= 0) {
|
||
return {
|
||
balance: null,
|
||
currency: 'USD',
|
||
quota: {
|
||
daily: Infinity,
|
||
used: Number.isFinite(dailyUsage) ? dailyUsage : 0,
|
||
remaining: Infinity,
|
||
percentage: 0,
|
||
unlimited: true
|
||
},
|
||
queryMethod: 'field'
|
||
}
|
||
}
|
||
|
||
const used = Number.isFinite(dailyUsage) ? dailyUsage : 0
|
||
const remaining = Math.max(0, dailyQuota - used)
|
||
const percentage = dailyQuota > 0 ? (used / dailyQuota) * 100 : 0
|
||
|
||
return {
|
||
balance: remaining,
|
||
currency: 'USD',
|
||
quota: {
|
||
daily: dailyQuota,
|
||
used,
|
||
remaining,
|
||
percentage: Math.round(percentage * 100) / 100
|
||
},
|
||
queryMethod: 'field'
|
||
}
|
||
}
|
||
|
||
parseCurrency(data) {
|
||
return data?.currency || data?.Currency || 'USD'
|
||
}
|
||
|
||
async safeExecute(fn, fallbackValue = null) {
|
||
try {
|
||
return await fn()
|
||
} catch (error) {
|
||
this.logger.error(`余额 Provider 执行失败: ${this.platform}`, error)
|
||
return fallbackValue
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = BaseBalanceProvider
|