421 lines
13 KiB
JavaScript
421 lines
13 KiB
JavaScript
import fs from 'fs';
|
||
import path from 'path';
|
||
import { logInfo, logDebug, logError } from './logger.js';
|
||
import { getNextProxyAgent } from './proxy-manager.js';
|
||
import { getRefreshConfig, requestRefreshToken } from './refresh-client.js';
|
||
|
||
/**
|
||
* Account Manager - 管理多个 OAuth 账号的选择、刷新和统计
|
||
*
|
||
* 设计思路:
|
||
* 1. 支持多个 refresh_token 账号池
|
||
* 2. 基于健康度加权轮询选择账号
|
||
* 3. 自动刷新 access_token
|
||
* 4. 401/402 时自动禁用异常账号
|
||
*/
|
||
|
||
const REFRESH_URL = 'https://api.workos.com/user_management/authenticate';
|
||
const REFRESH_INTERVAL_HOURS = 6;
|
||
const CLIENT_ID = 'client_01HNM792M5G5G1A2THWPXKFMXB';
|
||
|
||
class AccountManager {
|
||
constructor() {
|
||
this.accounts = [];
|
||
this.refreshLocks = new Map(); // 刷新锁,避免同一账号并发刷新
|
||
this.settings = {
|
||
algorithm: 'weighted', // 'weighted' or 'simple'
|
||
refresh_interval_hours: REFRESH_INTERVAL_HOURS,
|
||
disable_on_401: true,
|
||
disable_on_402: true
|
||
};
|
||
this.simpleIndex = 0;
|
||
this.configPath = null;
|
||
this.endpointStats = {}; // 端点统计
|
||
}
|
||
|
||
/**
|
||
* 从配置文件加载账号
|
||
*/
|
||
async loadAccounts(configPath = null) {
|
||
// 优先检查环境变量中的多账号配置
|
||
const envAccounts = process.env.OAUTH_ACCOUNTS;
|
||
if (envAccounts) {
|
||
try {
|
||
const parsed = JSON.parse(envAccounts);
|
||
this.accounts = this._normalizeAccounts(parsed);
|
||
logInfo(`AccountManager: 从环境变量加载了 ${this.accounts.length} 个账号`);
|
||
return true;
|
||
} catch (e) {
|
||
logError('AccountManager: 解析 OAUTH_ACCOUNTS 环境变量失败', e);
|
||
}
|
||
}
|
||
|
||
// 检查配置文件
|
||
const possiblePaths = [
|
||
configPath,
|
||
path.join(process.cwd(), 'accounts.json'),
|
||
path.join(process.cwd(), 'oauth_accounts.json')
|
||
].filter(Boolean);
|
||
|
||
for (const p of possiblePaths) {
|
||
if (fs.existsSync(p)) {
|
||
try {
|
||
const content = fs.readFileSync(p, 'utf-8');
|
||
const data = JSON.parse(content);
|
||
|
||
this.accounts = this._normalizeAccounts(data.accounts || data);
|
||
if (data.settings) {
|
||
this.settings = { ...this.settings, ...data.settings };
|
||
}
|
||
this.configPath = p;
|
||
|
||
logInfo(`AccountManager: 从 ${p} 加载了 ${this.accounts.length} 个账号`);
|
||
return true;
|
||
} catch (e) {
|
||
logError(`AccountManager: 读取配置文件 ${p} 失败`, e);
|
||
}
|
||
}
|
||
}
|
||
|
||
logInfo('AccountManager: 未找到多账号配置,将回退到单账号模式');
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 标准化账号数据结构
|
||
*/
|
||
_normalizeAccounts(accounts) {
|
||
if (!Array.isArray(accounts)) {
|
||
accounts = [accounts];
|
||
}
|
||
|
||
return accounts.map((acc, index) => ({
|
||
id: acc.id || `account_${index + 1}`,
|
||
name: acc.name || `账号${index + 1}`,
|
||
refresh_token: acc.refresh_token,
|
||
access_token: acc.access_token || null,
|
||
last_refresh: acc.last_refresh ? new Date(acc.last_refresh).getTime() : null,
|
||
status: acc.status || 'active', // 'active', 'disabled', 'rate_limited'
|
||
disable_reason: acc.disable_reason || null,
|
||
stats: acc.stats || { success: 0, fail: 0 }
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 获取活跃账号列表
|
||
*/
|
||
getActiveAccounts() {
|
||
return this.accounts.filter(acc => acc.status === 'active');
|
||
}
|
||
|
||
/**
|
||
* 选择一个账号(基于配置的算法)
|
||
*/
|
||
selectAccount() {
|
||
const activeAccounts = this.getActiveAccounts();
|
||
|
||
if (activeAccounts.length === 0) {
|
||
throw new Error('AccountManager: 没有可用账号 - 所有账号已被禁用');
|
||
}
|
||
|
||
if (activeAccounts.length === 1) {
|
||
return activeAccounts[0];
|
||
}
|
||
|
||
if (this.settings.algorithm === 'simple') {
|
||
return this._simpleSelect(activeAccounts);
|
||
} else {
|
||
return this._weightedSelect(activeAccounts);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 简单轮询算法
|
||
*/
|
||
_simpleSelect(accounts) {
|
||
const account = accounts[this.simpleIndex % accounts.length];
|
||
this.simpleIndex = (this.simpleIndex + 1) % accounts.length;
|
||
logDebug(`AccountManager: 简单轮询选择账号 ${account.id}`);
|
||
return account;
|
||
}
|
||
|
||
/**
|
||
* 基于健康度的加权选择算法
|
||
*/
|
||
_weightedSelect(accounts) {
|
||
const weights = accounts.map(acc => {
|
||
const total = acc.stats.success + acc.stats.fail;
|
||
if (total === 0) {
|
||
return 1; // 新账号默认权重
|
||
}
|
||
// 成功率 + 0.1(确保失败账号也有机会恢复)
|
||
return (acc.stats.success / total) + 0.1;
|
||
});
|
||
|
||
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
|
||
let random = Math.random() * totalWeight;
|
||
|
||
for (let i = 0; i < weights.length; i++) {
|
||
random -= weights[i];
|
||
if (random <= 0) {
|
||
const acc = accounts[i];
|
||
logDebug(`AccountManager: 加权选择账号 ${acc.id}, 健康度: ${((weights[i] - 0.1) * 100).toFixed(1)}%`);
|
||
return acc;
|
||
}
|
||
}
|
||
|
||
return accounts[accounts.length - 1];
|
||
}
|
||
|
||
/**
|
||
* 获取账号的 access_token(自动刷新)
|
||
*/
|
||
async getAccessToken(account) {
|
||
// 检查是否需要刷新
|
||
const needsRefresh = !account.access_token || this._shouldRefresh(account);
|
||
|
||
if (needsRefresh) {
|
||
await this._refreshTokenWithLock(account);
|
||
}
|
||
|
||
return account.access_token;
|
||
}
|
||
|
||
/**
|
||
* 带锁刷新,避免同一账号并发刷新
|
||
*/
|
||
async _refreshTokenWithLock(account) {
|
||
const existing = this.refreshLocks.get(account.id);
|
||
if (existing) {
|
||
return existing;
|
||
}
|
||
|
||
const refreshPromise = this._refreshToken(account)
|
||
.finally(() => {
|
||
this.refreshLocks.delete(account.id);
|
||
});
|
||
|
||
this.refreshLocks.set(account.id, refreshPromise);
|
||
return refreshPromise;
|
||
}
|
||
|
||
/**
|
||
* 检查是否需要刷新 token
|
||
*/
|
||
_shouldRefresh(account) {
|
||
if (!account.last_refresh) {
|
||
return true;
|
||
}
|
||
const hoursSinceRefresh = (Date.now() - account.last_refresh) / (1000 * 60 * 60);
|
||
return hoursSinceRefresh >= this.settings.refresh_interval_hours;
|
||
}
|
||
|
||
/**
|
||
* 刷新账号的 access_token
|
||
*/
|
||
async _refreshToken(account) {
|
||
logInfo(`AccountManager: 刷新账号 ${account.id} 的 token...`);
|
||
|
||
try {
|
||
const proxyAgentInfo = getNextProxyAgent(REFRESH_URL);
|
||
const refreshConfig = getRefreshConfig();
|
||
const data = await requestRefreshToken({
|
||
refreshUrl: REFRESH_URL,
|
||
refreshToken: account.refresh_token,
|
||
clientId: CLIENT_ID,
|
||
proxyAgentInfo,
|
||
...refreshConfig
|
||
});
|
||
|
||
// 更新账号信息
|
||
account.access_token = data.access_token;
|
||
account.refresh_token = data.refresh_token; // 更新 refresh_token
|
||
account.last_refresh = Date.now();
|
||
|
||
// 记录用户信息
|
||
if (data.user) {
|
||
account.email = data.user.email;
|
||
logInfo(`AccountManager: 账号 ${account.id} 刷新成功 - ${data.user.email}`);
|
||
}
|
||
|
||
// 保存配置
|
||
this._saveConfig();
|
||
|
||
return data.access_token;
|
||
|
||
} catch (error) {
|
||
logError(`AccountManager: 账号 ${account.id} 刷新失败`, error);
|
||
|
||
// 刷新失败可能是 refresh_token 失效,禁用账号
|
||
if (error.message.includes('401') || error.message.includes('invalid')) {
|
||
this.disableAccount(account.id, 'refresh_token_invalid');
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 记录请求结果
|
||
*/
|
||
recordResult(accountId, endpoint, success, statusCode) {
|
||
const account = this.accounts.find(acc => acc.id === accountId);
|
||
if (!account) return;
|
||
|
||
if (success) {
|
||
account.stats.success++;
|
||
} else {
|
||
account.stats.fail++;
|
||
|
||
// 检查是否需要禁用账号
|
||
if (statusCode === 401 && this.settings.disable_on_401) {
|
||
this.disableAccount(accountId, '401_unauthorized');
|
||
} else if (statusCode === 402 && this.settings.disable_on_402) {
|
||
this.disableAccount(accountId, '402_payment_required');
|
||
}
|
||
}
|
||
|
||
// 更新端点统计
|
||
if (!this.endpointStats[endpoint]) {
|
||
this.endpointStats[endpoint] = { success: 0, fail: 0 };
|
||
}
|
||
if (success) {
|
||
this.endpointStats[endpoint].success++;
|
||
} else {
|
||
this.endpointStats[endpoint].fail++;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 禁用账号
|
||
*/
|
||
disableAccount(accountId, reason) {
|
||
const account = this.accounts.find(acc => acc.id === accountId);
|
||
if (!account) return;
|
||
|
||
account.status = 'disabled';
|
||
account.disable_reason = reason;
|
||
account.disabled_at = new Date().toISOString();
|
||
|
||
logError(`AccountManager: 账号 ${accountId} 已禁用,原因: ${reason}`);
|
||
|
||
// 保存配置
|
||
this._saveConfig();
|
||
|
||
// 记录到废弃账号文件
|
||
this._logDeprecatedAccount(account);
|
||
}
|
||
|
||
/**
|
||
* 记录废弃账号到文件
|
||
*/
|
||
_logDeprecatedAccount(account) {
|
||
try {
|
||
const logPath = path.join(process.cwd(), 'deprecated_accounts.txt');
|
||
const logEntry = `[${new Date().toISOString()}] ${account.id} (${account.email || 'unknown'}) - ${account.disable_reason}\n`;
|
||
fs.appendFileSync(logPath, logEntry, 'utf-8');
|
||
} catch (e) {
|
||
logError('AccountManager: 记录废弃账号失败', e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存配置到文件
|
||
*/
|
||
_saveConfig() {
|
||
if (!this.configPath) return;
|
||
|
||
try {
|
||
const data = {
|
||
accounts: this.accounts.map(acc => ({
|
||
id: acc.id,
|
||
name: acc.name,
|
||
refresh_token: acc.refresh_token,
|
||
access_token: acc.access_token,
|
||
last_refresh: acc.last_refresh ? new Date(acc.last_refresh).toISOString() : null,
|
||
status: acc.status,
|
||
disable_reason: acc.disable_reason,
|
||
email: acc.email,
|
||
stats: acc.stats
|
||
})),
|
||
settings: this.settings,
|
||
last_updated: new Date().toISOString()
|
||
};
|
||
|
||
fs.writeFileSync(this.configPath, JSON.stringify(data, null, 2), 'utf-8');
|
||
logDebug(`AccountManager: 配置已保存到 ${this.configPath}`);
|
||
} catch (e) {
|
||
logError('AccountManager: 保存配置失败', e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取状态信息(用于 /status 页面)
|
||
*/
|
||
getStatus() {
|
||
return {
|
||
total_accounts: this.accounts.length,
|
||
active_accounts: this.getActiveAccounts().length,
|
||
algorithm: this.settings.algorithm,
|
||
accounts: this.accounts.map(acc => ({
|
||
id: acc.id,
|
||
name: acc.name,
|
||
email: acc.email || 'unknown',
|
||
status: acc.status,
|
||
disable_reason: acc.disable_reason,
|
||
stats: acc.stats,
|
||
health: this._calculateHealth(acc),
|
||
last_refresh: acc.last_refresh ? new Date(acc.last_refresh).toISOString() : null
|
||
})),
|
||
endpoint_stats: this.endpointStats
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 计算账号健康度
|
||
*/
|
||
_calculateHealth(account) {
|
||
const total = account.stats.success + account.stats.fail;
|
||
if (total === 0) return 100;
|
||
return Math.round((account.stats.success / total) * 100);
|
||
}
|
||
|
||
/**
|
||
* 检查是否有多账号配置
|
||
*/
|
||
hasMultipleAccounts() {
|
||
return this.accounts.length > 1;
|
||
}
|
||
|
||
/**
|
||
* 获取账号数量
|
||
*/
|
||
getAccountCount() {
|
||
return this.accounts.length;
|
||
}
|
||
}
|
||
|
||
// 单例实例
|
||
let accountManagerInstance = null;
|
||
|
||
/**
|
||
* 获取 AccountManager 实例
|
||
*/
|
||
export function getAccountManager() {
|
||
if (!accountManagerInstance) {
|
||
accountManagerInstance = new AccountManager();
|
||
}
|
||
return accountManagerInstance;
|
||
}
|
||
|
||
/**
|
||
* 初始化 AccountManager
|
||
*/
|
||
export async function initializeAccountManager() {
|
||
const manager = getAccountManager();
|
||
const loaded = await manager.loadAccounts();
|
||
return loaded;
|
||
}
|
||
|
||
export default AccountManager;
|